<?php

class Route {

  protected $_defaultRegex = null;
  protected $_defaults;
  protected $_variables = array();
  protected $_parts = array();
  protected $_requirements = array();
  protected $_values = array();
  protected $_staticCount = 0;
  protected $_wildcardData = array();
  protected $_matchedPath = null;
  protected static $_routes = array();

  /**
   *
   * @param  string  $name
   * @param  string  $pattern
   * @param  array   $defaults
   * @param  array   $reqs
   */
  public static function add($name, $route, $defaults = NULL, $reqs = NULL) {
    self::$_routes[$name] = new self($route, $defaults, $reqs);
  }

  /**
   *
   * @return  array
   */
  public static function all() {
    return self::$_routes;
  }

  /**
   *
   * @param  string  $name
   * @return  Route  FOUND
   * @return  NULL   FAILURE
   */
  public static function get($name) {
    return @self::$_routes[$name];
  }

  /**
   *
   * @param  string  $pattern
   * @param  array   $defaults
   * @param  array   $reqs
   */
  public function __construct($route, $defaults = NULL, $reqs = NULL) {

    $this->_defaults = (array) $defaults;
    $this->_requirements = (array) $reqs;
    $route = trim($route, '/');

    if ($route == '') {
      return;
    }

    foreach (explode('/', $route) as $pos => $part) {
      if (substr($part, 0, 1) == ':' && substr($part, 1, 1) != ':') {
        $name = substr($part, 1);
        $this->_parts[$pos] = (isset($reqs[$name]) ? $reqs[$name] : $this->_defaultRegex);
        $this->_variables[$pos] = $name;
      } else {
        if (substr($part, 0, 1) == ':') {
          $part = substr($part, 1);
        }
        $this->_parts[$pos] = $part;
        if ($part !== '*') {
          $this->_staticCount++;
        }
      }
    }
  }

  /**
   * assemble to uri
   * @param  array  $params
   */
  public function uri($data = array(), $reset = false, $encode = false, $partial = false) {
    $url = array();
    $flag = false;

    foreach ($this->_parts as $key => $part) {
      $name = isset($this->_variables[$key]) ? $this->_variables[$key] : null;
      $useDefault = false;
      if (isset($name) && array_key_exists($name, $data) && $data[$name] === null) {
        $useDefault = true;
      }
      if (isset($name)) {
        if (isset($data[$name]) && !$useDefault) {
          $value = $data[$name];
          unset($data[$name]);
        } elseif (!$reset && !$useDefault && isset($this->_values[$name])) {
          $value = $this->_values[$name];
        } elseif (!$reset && !$useDefault && isset($this->_wildcardData[$name])) {
          $value = $this->_wildcardData[$name];
        } elseif (isset($this->_defaults[$name])) {
          $value = $this->_defaults[$name];
        } else {
          throw new Exception($name . ' is not specified');
        }
        $url[$key] = $value;
      } elseif ($part != '*') {
        if (substr($part, 0, 2) === '@@') {
          $part = substr($part, 1);
        }
        $url[$key] = $part;
      } else {
        if (!$reset)
          $data += $this->_wildcardData;
        foreach ($data as $var => $value) {
          if ($value !== null && (!isset($this->_defaults[$var]) || $value != $this->_defaults[$var])) {
            $url[$key++] = $var;
            $url[$key++] = $value;
            $flag = true;
          }
        }
      }
    }

    $return = '';

    foreach (array_reverse($url, true) as $key => $value) {
      $defaultValue = null;

      if (isset($this->_variables[$key])) {
        $defaultValue = @$this->_defaults[$this->_variables[$key]];
      }

      if ($flag || $value !== $defaultValue || $partial) {
        if ($encode)
          $value = urlencode($value);
        $return = '/' . $value . $return;
        $flag = true;
      }
    }

    return trim($return, '/');
  }

  /**
   * @param string $path Path used to match against this routing map
   * @return array|false An array of assigned values or a false on a mismatch
   */
  public function match($path, $partial = false) {

    $pathStaticCount = 0;
    $values = array();
    $matchedPath = '';

    if (!$partial) {
      $path = trim($path, '/');
    }

    if ($path !== '') {
      $path = explode('/', $path);

      foreach ($path as $pos => $pathPart) {
        // Path is longer than a route, it's not a match
        if (!array_key_exists($pos, $this->_parts)) {
          if ($partial) {
            break;
          } else {
            return false;
          }
        }

        $matchedPath .= $pathPart . '/';

        // If it's a wildcard, get the rest of URL as wildcard data and stop matching
        if ($this->_parts[$pos] == '*') {
          $count = count($path);
          for ($i = $pos; $i < $count; $i+=2) {
            $var = urldecode($path[$i]);
            if (!isset($this->_wildcardData[$var]) && !isset($this->_defaults[$var]) && !isset($values[$var])) {
              $this->_wildcardData[$var] = (isset($path[$i + 1])) ? urldecode($path[$i + 1]) : null;
            }
          }

          $matchedPath = implode('/', $path);
          break;
        }

        $name = isset($this->_variables[$pos]) ? $this->_variables[$pos] : null;
        $pathPart = urldecode($pathPart);

        // Translate value if required
        $part = $this->_parts[$pos];

        // If it's a static part, match directly
        if ($name === null && $part != $pathPart) {
          return false;
        }

        // If it's a variable with requirement, match a regex. If not - everything matches
        if ($part !== null && !preg_match('#^' . $part . '$#iu', $pathPart)) {
          return false;
        }

        // If it's a variable store it's value for later
        if ($name !== null) {
          $values[$name] = $pathPart;
        } else {
          $pathStaticCount++;
        }
      }
    }

    // Check if all static mappings have been matched
    if ($this->_staticCount != $pathStaticCount) {
      return false;
    }

    $return = $values + $this->_wildcardData + $this->_defaults;

    // Check if all map variables have been initialized
    foreach ($this->_variables as $var) {
      if (!array_key_exists($var, $return)) {
        return false;
      } elseif ($return[$var] == '' || $return[$var] === null) {
        // Empty variable? Replace with the default value.
        $return[$var] = $this->_defaults[$var];
      }
    }

    $this->setMatchedPath(rtrim($matchedPath, '/'));

    $this->_values = $values;

    return $return;
  }

  public function setMatchedPath($path) {
    $this->_matchedPath = $path;
  }

  protected function _url($params, $host, $protocol) {
    
    $uri = $this->uri($params);
    if ($host == NULL) {
      $host = HOST;
    }
    if ($protocol == NULL) {
      $protocol = PROTOCOL;
    }
    return $protocol . $host . BASE_URL . $uri;
  }

  /**
   *
   * @param  array|string  $params
   * @param  string $name
   * @param  string $host
   * @param  string $protocol
   * @return string
   */
  public static function url($params, $name = 'default', $host = NULL, $protocol = NULL) {
    
    if (is_string($params)) {
      return self::baseUrl() . $params;
    }
    
    if (($route = self::get($name)) != NULL) {
      return $route->_url($params, $host, $protocol);
    } else {
      echo '#';
    }
  }

  public static function baseUrl() {
    return BASE_URL;
  }

}